﻿using System;
using System.Collections.Generic;
using System.Data.SqlTypes;
using System.Diagnostics.Contracts;
using System.IO;
using System.Threading.Tasks;
using Wombat.Core.EventAggregator;
using Wombat.Data;
using Wombat.Data.Model;

namespace Wombat.DbToDwAdapter
{
    public class SyncToDwOrchestrator : IOrchestrator
    {
        private readonly IDal _dal;
        private readonly IFactAggregationService _factAggregationService;
        private readonly IEventPublisher _eventPublisher;
        private string _sourceConnectionString;
        private string _destinationConnectionSetring;

        public SyncToDwOrchestrator(IDal dal, IFactAggregationService factAggregationService, IEventPublisher eventPublisher)
        {
            _dal = dal;
            _factAggregationService = factAggregationService;
            _eventPublisher = eventPublisher;
        }

        public void SetSource(ISyncDataSource source)
        {
            Contract.Assert(source != null);
            _sourceConnectionString = source.ConnectionString;
        }

        public void SetDestination(ISyncDataSource destination)
        {
            Contract.Assert(destination != null);
            _destinationConnectionSetring = destination.ConnectionString;
        }

        public async Task OrchestrateAsync()
        {
            _eventPublisher.Publish(new DataMigrationEvent{Status = "Praparing publication."});
            await ImportTags();
            _eventPublisher.Publish(new DataMigrationEvent { Status = "Tags Imported." });
            await ImportLatestData();
            _eventPublisher.Publish(new DataMigrationEvent { Status = "All Data Imported." });
        }

        private async Task ImportLatestData()
        {
            using (var conn = await _dal.Connect(_sourceConnectionString))
            {
                using (var destConn = await _dal.Connect(_destinationConnectionSetring))
                {
                    var lastFloatData = await _dal.ExecuteScalarAsync<long>(destConn, Queries.GetLastReadFloat);
                    var factTime = await _dal.ExecuteScalarAsync<DateTime>(destConn, Queries.GetLastFactTime);
                    var minTime = factTime == DateTime.MinValue ? (DateTime?)null : factTime;
                    var maxId = default(long?);
                    var maxTime = default(DateTime?);
                    var data = await _dal.ExecuteReaderReadAllAsync(conn,
                                                                    string.Format(Queries.GetRecentFloatData, lastFloatData),
                                                                    r =>
                                                                        {
                                                                            var fd = new RawFloatData
                                                                                {
                                                                                    DataId = r.GetInt64(0),
                                                                                    TagId = r.GetInt64(1),
                                                                                    Time = r.GetDateTime(2),
                                                                                    Value = FixNull(r.GetSqlDouble(3)),
                                                                                    LeftDeviation =
                                                                                        FixNull(r.GetSqlDouble(4)),
                                                                                    Flag = r.GetByte(5)
                                                                                };
                                                                            maxId = Math.Max(maxId ?? long.MinValue, fd.DataId);
                                                                            maxTime = fd.Time > (maxTime ?? DateTime.MinValue) ? fd.Time : maxTime;
                                                                            return fd;
                                                                        });
                    //var interpolatedData = InterpolateFloatData(data, factTime == default(DateTime) ? (DateTime?) null : factTime
                    //    , GetFactTime(DateTime.UtcNow, InterpolationMinutes).AddMinutes(-InterpolationMinutes));
                    //await _dal.ExecuteNotQueryAsync(destConn, Queries.CreateAndDropTmpFloatDataTable);
                    await _dal.BulkInsertAsync(destConn, "FloatData", data,
                                               t => string.Format("{0}\t{1}\t{2}\t{3}\t{4}\t{5}",
                                                   t.DataId, 
                                                   t.Time.ToString(TimeFormatString), 
                                                   t.Value, 
                                                   t.LeftDeviation,
                                                   t.TagId,
                                                   t.Flag));
                    if (maxId == null || maxTime == null)
                        throw new InvalidOperationException("No data was migrated!"); 
                    await _dal.ExecuteNotQueryAsync(destConn, string.Format(Queries.MergeTmpFloatData, lastFloatData));
                    await _dal.ExecuteNotQueryAsync(destConn, string.Format(Queries.AddSyncHistoryForFloatData, maxId, maxTime.Value.ToString(TimeFormatString)));
                }
            }
        }

        private const string TimeFormatString = "dd MMM yyyy HH:mm:ss.fff";

        private const int InterpolationMinutes = 15;

        private IEnumerable<InterpolatedFloatData> InterpolateFloatData(IEnumerable<RawFloatData> data, DateTime? lastFactTime, DateTime interpolateTo)
        {
            var tagLastInterpolated = new Dictionary<long, DateTime>();
            var tagLastData = new Dictionary<long, RawFloatData>();
            foreach (var rawFloatData in data)
            {
                lastFactTime = lastFactTime ?? GetFactTime(rawFloatData.Time.AddMinutes(-InterpolationMinutes), InterpolationMinutes);
                if (rawFloatData.Time == default(DateTime))
                    throw new InvalidDataException(string.Format("Data with DataId {0} has invalid date.", rawFloatData.DataId));
                if (!tagLastInterpolated.ContainsKey(rawFloatData.TagId))
                    tagLastInterpolated.Add(rawFloatData.TagId, lastFactTime.Value);
                if (!tagLastData.ContainsKey(rawFloatData.TagId))
                    tagLastData.Add(rawFloatData.TagId, rawFloatData);
                else
                    tagLastData[rawFloatData.TagId] = rawFloatData;

                var tagLastInterploation = tagLastInterpolated[rawFloatData.TagId];
                var expectedMinute = GetFactTime(rawFloatData.Time, InterpolationMinutes);
                if (expectedMinute > tagLastInterploation.AddMinutes(InterpolationMinutes))
                {
                    var returnedAggregations = default(IEnumerable<InterpolatedFloatData>);
                    returnedAggregations = _factAggregationService.GetAggregatedFloatData(rawFloatData, tagLastInterploation, expectedMinute.AddMinutes(-InterpolationMinutes),
                                                                                          InterpolationMinutes);
                    foreach (var interpolatedFloatData in returnedAggregations)
                    {
                        yield return interpolatedFloatData;
                    }
                    tagLastInterpolated[rawFloatData.TagId] = expectedMinute.AddMinutes(- InterpolationMinutes);
                }
            }

            foreach (var tagId in tagLastInterpolated.Keys)
            {
                var tagLastInterploation = tagLastInterpolated[tagId];
                if (tagLastInterploation >= interpolateTo) continue;
                var returnedAggregations = default(IEnumerable<InterpolatedFloatData>);
                returnedAggregations = _factAggregationService.GetAggregatedFloatData(tagLastData[tagId],
                                                                                      tagLastInterploation,
                                                                                      interpolateTo,
                                                                                      InterpolationMinutes);
                foreach (var interpolatedFloatData in returnedAggregations)
                {
                    yield return interpolatedFloatData;
                }
            }
        }

        private DateTime GetFactTime(DateTime startTime, int interpolationMinutes)
        {
            var time = startTime.AddMinutes(interpolationMinutes);
            return new DateTime(time.Year, time.Month, time.Day, time.Hour,
                                (time.Minute/interpolationMinutes)*interpolationMinutes, 0);
        }



        private async Task ImportTags()
        {
            using (var conn = await _dal.Connect(_sourceConnectionString))
            {
                using (var destConn = await _dal.Connect(_destinationConnectionSetring))
                {
                    var tags = await _dal.ExecuteReaderAsync(conn,
                                                             string.Format(Queries.GetAllTags, (int)RelationshipType.ParentChild),
                                                             r =>
                                                             new TagData
                                                                 {
                                                                     TagId = r.GetInt64(0),
                                                                     Name = r.GetString(1),
                                                                     DisplayName = r.GetString(2),
                                                                     ParentId = FixNull(r.GetSqlInt64(3)),
                                                                     Unit = r.GetString(4)
                                                                 });
                    await _dal.ExecuteNotQueryAsync(destConn, Queries.DropAndCreateTmpTagTable);
                    await _dal.BulkInsertAsync(destConn, "tmp_Tags", tags,
                                               t => string.Format("{0}\t{1}\t{2}\t{3}\t{4}", t.TagId, t.Name, t.DisplayName, t.ParentId, t.Unit));
                    await _dal.ExecuteNotQueryAsync(destConn, Queries.MergeTmpTagsToTags);
                }
            }
        }

        private double? FixNull(SqlDouble data)
        {
            if (data.IsNull)
                return null;
            return data.Value;
        }

        private long? FixNull(SqlInt64 data)
        {
            if (data.IsNull)
                return null;
            return data.Value;
        }
    }
}